library(rvest)
library(tidyverse)
library(polite)
library(xml2)
library(chromote)
library(lubridate)Crosstech Solutions Group
Публикационная активность компании на Хабре
О компании
Crosstech Solutions Group (далее – CTSG/КТСГ) – российский разработчик инновационных решений для комплексной защиты информации бизнеса. Компания предлагает готовые решения, осуществляет заказную разработку и предоставляет ИТ-услуги в области кибербезопасности. Представлена на рынке ИБ с 2018 года.
Продуктовый портфель КТСГ на сегодняшний день насчитывает 11 продуктов. Все они разработаны на основе глубоких исследований рынка информационной безопасности и закрепили за собой статус высокоэффективных средств защиты информации, пройдя апробацию в крупнейших компаниях различных отраслей экономики России. ИБ-продукты включены в реестр отечественного ПО Минцифры России и рекомендованы для импортозамещения на российских предприятиях.
Компания активно развивается не только со стороны разработки и поставки ПО, но и со стороны PR и маркетинга. Так, компания ведет соц. сети для повышения узнаваемости продуктов и развития бренда:
- Telegram-канал;
- группа ВКонтакте;
- Хабр и др.
Цель исследования
В рамках учебной работы представлен анализ публикационной активности компании на Хабре. Я применила количественные методы, чтобы решить следующие задачи:
- определить самые популярные статьи компании с учетом нескольких показателей;
- выявить тематические направления, вызывающие наибольший интерес у аудитории;
- визуализировать полученные результаты.
Я получила большое удовольствое в процессе работы. Надеюсь, тебе понравятся ее результаты ;)
Поехали!
Веб-скрапинг
На начальном этапе я скрапила все опубликованные статьи компании на Хабре. В основном работала с пакетом rvest, но в одном случае понадобилось открыть сессию через chromote. Но об этом позже.
Сначала привязала все библиотеки.
Собрала названия всех статей и ссылки на них. Привела это к тайди-формату и подготовила первую таблицу.
url <- "https://habr.com/ru/companies/ctsg/articles/"
# создаю "виртуальный браузер" для легитимизации действий парсера
session <- bow(url)
html <- scrape(session)
# сохраняю себе названия статей, класс определила через SelectorGadget
elements <- html |>
html_elements(".tm-title__link")
# сохраняю таблицу, в которую складываю названия статей и ссылки на них
# все это хранится в теге "a"
articles <- tibble(
title = elements |>
html_text2(),
href = elements |>
html_attr("href")
)
# добавляю протокол и домен к "половинчатым" сслыкам
articles <- articles |>
mutate(link = str_c("https://habr.com", href)) |>
select(-href)
# сохраняю вектор ссылок на статьи
urls <- articles |>
pull(link)По итогу получилась такая табличка.
| title | link |
|---|---|
| Как создать решение в области контейнерной безопасности: подводные камни, проблемы и их решение | https://habr.com/ru/companies/ctsg/articles/972514/ |
| PAM в информационной безопасности: ценный актив или бесполезный сотрудник? | https://habr.com/ru/companies/ctsg/articles/918904/ |
| Хакатоны только для гениев? Разбираем самые популярные заблуждения | https://habr.com/ru/companies/ctsg/articles/893028/ |
| От идеи до первого выпуска: как и зачем мы запустили подкаст про ИБ? | https://habr.com/ru/companies/ctsg/articles/882176/ |
| Кто такие DevSecOps -инженеры и зачем они нужны? | https://habr.com/ru/companies/ctsg/articles/867704/ |
| Как с нуля построить систему обработки событий | https://habr.com/ru/companies/ctsg/articles/842186/ |
| Как организовать внутренний митап, чтобы он зашел команде? Наши принципы и немного истории | https://habr.com/ru/companies/ctsg/articles/831464/ |
| Хочу стать тимлидом: как выбрать свой путь от специалиста в руководители | https://habr.com/ru/companies/ctsg/articles/819323/ |
| Секреты успешного собеседования: как получить оффер технарю | https://habr.com/ru/companies/ctsg/articles/818243/ |
| Как тимлиду проводить собеседование так, чтобы кандидат и компания получили от него максимум | https://habr.com/ru/companies/ctsg/articles/808311/ |
| Распознавание лиц на микрокомпьютерах | https://habr.com/ru/companies/ctsg/articles/807069/ |
| Эффективные вложения в ИТ: Как посчитать ROI при внедрении ПО на примере системы маскирования данных | https://habr.com/ru/companies/ctsg/articles/805255/ |
| Как выжить на первом испытательном сроке в IT и не только | https://habr.com/ru/companies/ctsg/articles/803979/ |
Затем написала функцию, которая пройдется по всей таблице с ссылками на статьи и соберет нужные мне для анализа данные. В данном случае функция будет “вытаскивать”:
- тексты статей;
- дата;
- время публикации;
- время чтения;
- количество голосов (грубо говоря, лайков);
- количество добавлений в избранное;
- число комментариев;
- теги, хабы;
- уровень сложности текста.
get_article_polite <- function(url) {
bow_obj <- bow(url)
html_page <- scrape(bow_obj)
months_dict <- c(
"янв" = "01",
"фев" = "02",
"мар" = "03",
"апр" = "04",
"мая" = "05",
"июн" = "06",
"июл" = "07",
"авг" = "08",
"сен" = "09",
"окт" = "10",
"ноя" = "11",
"дек" = "12"
)
date = html_page |>
html_elements(".tm-article-presenter__snippet time") |>
html_text2() |>
str_replace(" в ", " ") |>
str_replace("\\d+:\\d+", "")
if (!str_detect(date, "202")) {date <- paste(date, year(Sys.Date()))}
res = tibble(
text = html_page |>
html_elements(".article-body") |>
html_text2() |>
str_squish() |>
str_remove("^\\[\\] "),
date = date |>
str_squish() |>
str_replace_all(months_dict) |>
dmy(),
time = html_page |>
html_elements(".tm-article-presenter__snippet time") |>
html_text2() |>
str_extract("\\d+:\\d+") |>
hm(),
read_time_min = html_page |>
html_elements(".tm-article-presenter__snippet .tm-article-reading-time__label") |>
html_text2() |>
str_extract("\\d+") |>
as.integer(),
votes = html_page |>
html_element(".tm-votes-lever__score_appearance-article span, .votes-switcher") |>
html_text2() |>
str_extract("(?<=\\+).") |> # без этой регулярки отображается доп. инфа, необходимо взять только последнее число
as.integer(),
mark = html_page |>
html_elements(".tm-article-sticky-panel__icons .bookmarks-button") |>
html_text2() |>
str_remove("Добавить в закладки") |>
as.integer(),
num_of_comments = html_page |>
html_elements(".tm-article-sticky-panel__icons .article-comments-counter-link-wrapper") |>
html_text2() |>
str_remove("Комментарии") |>
as.integer(),
tags = html_page |>
html_elements(".tag-list") |>
html_text2() |>
str_extract_all("(?<=\\[).*?(?=\\])") |>
unlist() |>
paste(collapse = ", "),
hubs = html_page |>
html_elements(".tm-article-presenter__meta-list+ .tm-article-presenter__meta-list") |>
html_text2() |>
str_extract_all("(?<=\\[).*?(?=\\])") |>
unlist() |>
paste(collapse = ", "),
complexity_label = html_page |>
html_element(".tm-article-complexity_complexity-medium .tm-article-complexity__label") |>
html_text2()
)
return(res)
}Применяю функцию ко всем статьям через итератор
articles_ctsg <- map_df(urls, get_article_polite, .id = "id")И в результате получаем следующее. В таблице ниже в некоторых ячейках обрезаны строки, чтобы визуальнее это смотрелось приятнее. На итоговый тиббл я оставлю ссылку для скачивания.
| id | text | date | time | read_time_min | votes | mark | num_of_comments | tags | hubs | complexity_label |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Всем привет! На связи Александр Синичкин, ведущий архитектор … | 2025-12-02 | 13H 44M 0S | 9 | 8 | 9 | 0 | контейнеризация, контейнерная безопасность, разработка продукта, кейс, container security, … | Блог компании Crosstech Solutions Group, Kubernetes, IT-инфраструктура, IT-компании | Средний |
| 2 | PAM или партнерский менеджер — специалист, отвечающий за … | 2025-06-17 | 7H 51M 0S | 7 | 5 | 2 | 4 | продажи в it, информационная безопасность, партнеры, партнерские отношения, … | Блог компании Crosstech Solutions Group, Информационная безопасность | NA |
| 3 | Хакатон — это марафон в мире IT. Здесь … | 2025-03-21 | 9H 30M 0S | 3 | 6 | 7 | 1 | хакатон, командообразование, защита проекта, тестировщик, студенты it, студенты | Блог компании Crosstech Solutions Group | NA |
| 4 | Привет! Это Яна Ильина, HRBP CrossTech Solutions Group, … | 2025-02-13 | 11H 29M 0S | 6 | 6 | 9 | 2 | подкаст, бренд, выпуски, команда, информационная безопасность, спикеры | Блог компании Crosstech Solutions Group, Информационная безопасность | NA |
| 5 | Добрый день, уважаемые читатели! Сегодня я расскажу о … | 2024-12-18 | 12H 30M 0S | 5 | NA | 13 | 3 | DevSecOps -инженеры, информационная безопасность, тестирование, уязвимости | Блог компании Crosstech Solutions Group | NA |
| 6 | Сегодня Александр Шувалов и Юлиян Латыпов поделятся с … | 2024-09-10 | 10H 24M 0S | 7 | 3 | 28 | 0 | данные, потоковая обработка, потоковая обработка данных | Блог компании Crosstech Solutions Group, Анализ и проектирование … | NA |
| 7 | Всем привет! Меня зовут Ульяна Петракова, я специалист … | 2024-07-25 | 13H 8M 0S | 2 | 4 | 8 | 1 | it-компании, карьера в it-индустрии, митап | Блог компании Crosstech Solutions Group, Управление персоналом | NA |
| 8 | Когда я работал программистом, мне было интересно не … | 2024-06-04 | 7H 50M 0S | 17 | 1 | 40 | 0 | управление, тимлидство, путь в ит, развитие, менеджмент, лидерство | Блог компании Crosstech Solutions Group, Управление разработкой, Управление … | NA |
| 9 | Привет! Меня зовут Артём и когда-то я уже … | 2024-05-30 | 8H 37M 0S | 11 | 7 | 44 | 8 | собеседование в it, подбор персонала, интервью, рекрутинг, интервью … | Блог компании Crosstech Solutions Group, Карьера в IT-индустрии, … | NA |
| 10 | Всем привет! С вами снова я, Артём Харченков, … | 2024-04-17 | 7H 48M 0S | 12 | 3 | 20 | 9 | карьера в it-индустрии, it компании, информационная безопасность | Блог компании Crosstech Solutions Group | NA |
| 11 | В последние годы появляется всё больше технологий с … | 2024-04-11 | 14H 8M 0S | 9 | 9 | 52 | 7 | информационная безопасность, распознавание лиц | Блог компании Crosstech Solutions Group, Машинное обучение | NA |
| 12 | Всем привет! Меня зовут Али Гаджиев, я Директор … | 2024-04-04 | 6H 28M 0S | 7 | 4 | 9 | 2 | защита данных, защита от утечек данных, субд | Блог компании Crosstech Solutions Group, Хранение данных | Средний |
| 13 | Всем привет! Меня зовут Артём Харченков, и я … | 2024-03-29 | 14H 36M 0S | 13 | 3 | 105 | 16 | испытательный срок, информационная безопасность, подбор персонала | Блог компании Crosstech Solutions Group, Информационная безопасность, Карьера … | NA |
Далее делаю кое-какие преобразования, чтобы можно было собрать отдельную статистику по дням и по времени.
months_dict2 <- c(
"янв" = "Jan",
"фев" = "Feb",
"мар" = "Mar",
"апр" = "Apr",
"май" = "May",
"июн" = "Jun",
"июл" = "Jul",
"авг" = "Aug",
"сен" = "Sep",
"окт" = "Oct",
"ноя" = "Nov",
"дек" = "Dec"
)
week_dict <- c(
"Пн" = "Mon",
"Вт" = "Tue",
"Ср" = "Wed",
"Чт" = "Thu",
"Пт" = "Fri",
"Сб" = "Sat",
"Вс" = "Sun"
)
articles_ctsg <- articles_ctsg |>
mutate(year = year(date),
month = month(date, label = TRUE),
wday = wday(date, label = TRUE, locale = Sys.getlocale("LC_TIME")),
hour = hour(time),
length = str_count(text, "\\S+")) |>
mutate(month = str_replace_all(as.character(month), months_dict2),
wday = str_replace_all(as.character(wday), week_dict))В Хабре не так давно прошло обновление. Теперь он показывает число уникальных пользователей, которые:
- Открыли публикацию;
- Открыли публикацию ИЛИ увидели еe в ленте.
Значит, второй показатель будет заведомо больше. И он позволит HR и PR-менеджерам делать дополнительные выводы в ходе анализа интересов аудитории.
Нажмите на картинку, чтобы ее приблизить.
Но сам этот элемент является popup-элементом. Т.е. он открывается только при нажатии не него. Через SelectorGadget у меня не получилось до него достучаться, поэтому я использовала пакет chromote.
И затем я извлекаю количество просмотров.
get_article_2 <- function(url) {
b <- ChromoteSession$new()
b$Network$enable()
b$Network$setBlockedURLs(urls = list(
"*googletagmanager.com/*",
"*google-analytics.com/*",
"*analytics.google.com/*",
"*mc.yandex.ru/*",
"*yandex.ru/ads/*",
"*vk.com/rtrg*",
"*sentry*"
))
b$Page$navigate(url)
b$Page$loadEventFired(wait_ = TRUE)
Sys.sleep(3)
CLICK_SELECTOR <- ".tm-article-presenter__snippet .reach-counter"
js_click <- sprintf("
(function(){
var el = document.querySelector('%s');
if (el) { el.click(); return 'clicked'; }
else { return 'not found'; }
})();
", CLICK_SELECTOR)
res <- b$Runtime$evaluate(js_click)
Sys.sleep(2)
html <- {
doc <- b$DOM$getDocument()
root_id <- doc$root$nodeId
b$DOM$getOuterHTML(nodeId = root_id)[['outerHTML']]
}
page <- read_html(html)
res = tibble(
reach_value = page |>
html_element(".tm-modal-window .value") |>
html_text2() |>
str_remove(" охват"),
unique_readers = page |>
html_element(".publication-metric+ .publication-metric .value") |>
html_text2() |>
str_remove(" читател(ей|я|ь)")
)
return(res)
}
articles_ctsg_2 <- map_df(urls, get_article_2, .id = "id")В хабре количество просмотров, превыщающее 1 тысячу, обозначается через К, например, 1.2к - 1200 просмотров. Ниже будет представлена функция, которая будет извлекать приблизительное число просмотров и переводить его в числовой формат. Да, значение получится округленным, но это зато примет более нормализованный вид.
change_views <- function(vec) {
map_int(vec, ~ {
x <- .x
# если запись содержит символ точки или "К"
if (is.na(as.integer(x)) == "TRUE") {
# сначала удаляю "K"
x <- str_remove(x, "K")
# если после удаления "К" есть еще один ненужный элемент - точка
if (str_detect(x, "\\.")) {
# то удаляем точку
x <- str_remove(x, "\\.")
# и приписываем два нуля (условно умножаем на тысячу)
x <- str_c(x, "00")
# переводим к целочисленному типу
x <- as.integer(x)
}
else {
# если точки нет, то просто приписываем 3 нуля (будто умножаем на тысячу)
# и переводим к целому типу
x <- str_c(x, "000")
x <- as.integer(x)
}
}
# если нет ни точки, ни "К", то просто переводим к целому типу данных
else {
x <- as.integer(x)
}
})
}Применяю функцию к полученному тибблу.
articles_ctsg_2 <- articles_ctsg_2 |>
mutate(reach_value = change_views(reach_value)) |>
mutate(unique_readers = change_views(unique_readers)) |>
mutate(id = as.integer(id))И получается такая милая табличка.
| id | reach_value | unique_readers |
|---|---|---|
| 1 | 7300 | 677 |
| 2 | 478 | 478 |
| 3 | 751 | 751 |
| 4 | 703 | 703 |
| 5 | 2100 | 2100 |
| 6 | 2200 | 2200 |
| 7 | 743 | 743 |
| 8 | 2100 | 2100 |
| 9 | 3800 | 3800 |
| 10 | 4000 | 4000 |
| 11 | 5400 | 5400 |
| 12 | 2900 | 2900 |
| 13 | 23000 | 23000 |
Обработка данных
После того как все данные собраны, я делаю дополнительные преобзования. Во-первых, разделяю колонки с датами на отдельные колонки, добавляю колонку с количеством слов в тексте, и заменяю значения месяцев и дней недели по словарям.
months_dict2 <- c(
"янв" = "Jan",
"фев" = "Feb",
"мар" = "Mar",
"апр" = "Apr",
"май" = "May",
"июн" = "Jun",
"июл" = "Jul",
"авг" = "Aug",
"сен" = "Sep",
"окт" = "Oct",
"ноя" = "Nov",
"дек" = "Dec"
)
week_dict <- c(
"Пн" = "Mon",
"Вт" = "Tue",
"Ср" = "Wed",
"Чт" = "Thu",
"Пт" = "Fri",
"Сб" = "Sat",
"Вс" = "Sun"
)
articles_ctsg <- articles_ctsg |>
mutate(year = year(date),
month = month(date, label = TRUE),
wday = wday(date, label = TRUE, locale = Sys.getlocale("LC_TIME")),
hour = hour(time),
length = str_count(text, "\\S+")) |>
mutate(month = str_replace_all(as.character(month), months_dict2),
wday = str_replace_all(as.character(wday), week_dict))Во-вторых, добавляю отдельную колонку с ID статьи. Также преобразовываю общую колонку с временем и отдельную колонку с часами: прибавляю +3 часа, чтобы отображаемое время публикации статьи было московским.
articles_ctsg <- articles_ctsg |>
mutate(id = as.integer(id),
time = time + hours(3),
hour = hour + 3)
# добавляю к исходному тибблу с названиями статей и ссылками на них идентификатор
articles <- articles |>
mutate(id = row_number())В-третьих, делаю два иннер-джойна (ведь таблиц у меня 3, за раз можно объединить только 2).
articles_ctsg <- articles_ctsg |>
inner_join(articles_ctsg_2, by = "id")
# объединяю две таблицы через inner join (т.к. здесь нет пропусков)
articles_ctsg_full <- inner_join(articles, articles_ctsg, by = "id") |>
select(id, title, text, length, link, complexity_label, date, year, month, wday, hour, time, read_time_min, reach_value, unique_readers, votes, mark, num_of_comments, tags, hubs)И у меня получается общая таблица со всеми собранными данными. Целая таблица для скачивания представлена по ссылке.
Обращаю внимание, что при воспроизведении кода результаты могут немного изменяться. Это связано с тем, что я локально прогнала этот код и подгрузила его в проект, чтобы облегчить работу рендеру. Поэтому у Вас могут быть другие показатели как минимум просмотров.
| id | title | text | length | link | complexity_label | date | year | month | wday | hour | time | read_time_min | reach_value | unique_readers | votes | mark | num_of_comments | tags | hubs |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Как создать решение в области контейнерной безопасности: подводные камни, проблемы и их решение | Всем привет! На связи Александр Синичкин, ведущий архитектор … | 1851 | https://habr.com/ru/... | Средний | 2025-12-02 | 2025 | Dec | Tue | 16 | 16H 44M 0S | 9 | 7300 | 677 | 8 | 9 | 0 | контейнеризация, контейнерная безопасность, … | Блог компании Crosstech … |
| 2 | PAM в информационной безопасности: ценный актив или бесполезный сотрудник? | PAM или партнерский менеджер — специалист, отвечающий за … | 1420 | https://habr.com/ru/... | NA | 2025-06-17 | 2025 | Jun | Tue | 10 | 10H 51M 0S | 7 | 478 | 478 | 5 | 2 | 4 | продажи в it, … | Блог компании Crosstech … |
| 3 | Хакатоны только для гениев? Разбираем самые популярные заблуждения | Хакатон — это марафон в мире IT. Здесь … | 652 | https://habr.com/ru/... | NA | 2025-03-21 | 2025 | Mar | Fri | 12 | 12H 30M 0S | 3 | 751 | 751 | 6 | 7 | 1 | хакатон, командообразование, защита … | Блог компании Crosstech … |
| 4 | От идеи до первого выпуска: как и зачем мы запустили подкаст про ИБ? | Привет! Это Яна Ильина, HRBP CrossTech Solutions Group, … | 1172 | https://habr.com/ru/... | NA | 2025-02-13 | 2025 | Feb | Thu | 14 | 14H 29M 0S | 6 | 703 | 703 | 6 | 9 | 2 | подкаст, бренд, выпуски, … | Блог компании Crosstech … |
| 5 | Кто такие DevSecOps -инженеры и зачем они нужны? | Добрый день, уважаемые читатели! Сегодня я расскажу о … | 1033 | https://habr.com/ru/... | NA | 2024-12-18 | 2024 | Dec | Wed | 15 | 15H 30M 0S | 5 | 2100 | 2100 | NA | 13 | 3 | DevSecOps -инженеры, информационная … | Блог компании Crosstech … |
| 6 | Как с нуля построить систему обработки событий | Сегодня Александр Шувалов и Юлиян Латыпов поделятся с … | 1354 | https://habr.com/ru/... | NA | 2024-09-10 | 2024 | Sep | Tue | 13 | 13H 24M 0S | 7 | 2200 | 2200 | 3 | 28 | 0 | данные, потоковая обработка, … | Блог компании Crosstech … |
| 7 | Как организовать внутренний митап, чтобы он зашел команде? Наши принципы и немного истории | Всем привет! Меня зовут Ульяна Петракова, я специалист … | 367 | https://habr.com/ru/... | NA | 2024-07-25 | 2024 | Jul | Thu | 16 | 16H 8M 0S | 2 | 743 | 743 | 4 | 8 | 1 | it-компании, карьера в … | Блог компании Crosstech … |
| 8 | Хочу стать тимлидом: как выбрать свой путь от специалиста в руководители | Когда я работал программистом, мне было интересно не … | 4323 | https://habr.com/ru/... | NA | 2024-06-04 | 2024 | Jun | Tue | 10 | 10H 50M 0S | 17 | 2100 | 2100 | 1 | 40 | 0 | управление, тимлидство, путь … | Блог компании Crosstech … |
| 9 | Секреты успешного собеседования: как получить оффер технарю | Привет! Меня зовут Артём и когда-то я уже … | 2766 | https://habr.com/ru/... | NA | 2024-05-30 | 2024 | May | Thu | 11 | 11H 37M 0S | 11 | 3800 | 3800 | 7 | 44 | 8 | собеседование в it, … | Блог компании Crosstech … |
| 10 | Как тимлиду проводить собеседование так, чтобы кандидат и компания получили от него максимум | Всем привет! С вами снова я, Артём Харченков, … | 3032 | https://habr.com/ru/... | NA | 2024-04-17 | 2024 | Apr | Wed | 10 | 10H 48M 0S | 12 | 4000 | 4000 | 3 | 20 | 9 | карьера в it-индустрии, … | Блог компании Crosstech … |
| 11 | Распознавание лиц на микрокомпьютерах | В последние годы появляется всё больше технологий с … | 2022 | https://habr.com/ru/... | NA | 2024-04-11 | 2024 | Apr | Thu | 17 | 17H 8M 0S | 9 | 5400 | 5400 | 9 | 52 | 7 | информационная безопасность, распознавание … | Блог компании Crosstech … |
| 12 | Эффективные вложения в ИТ: Как посчитать ROI при внедрении ПО на примере системы маскирования данных | Всем привет! Меня зовут Али Гаджиев, я Директор … | 1633 | https://habr.com/ru/... | Средний | 2024-04-04 | 2024 | Apr | Thu | 9 | 9H 28M 0S | 7 | 2900 | 2900 | 4 | 9 | 2 | защита данных, защита … | Блог компании Crosstech … |
| 13 | Как выжить на первом испытательном сроке в IT и не только | Всем привет! Меня зовут Артём Харченков, и я … | 3348 | https://habr.com/ru/... | NA | 2024-03-29 | 2024 | Mar | Fri | 17 | 17H 36M 0S | 13 | 23000 | 23000 | 3 | 105 | 16 | испытательный срок, информационная … | Блог компании Crosstech … |
Анализ данных
Теперь мне было интересно узнать, какие статьи компании оказались самыми популярными. Выводы необходимо делать на основании тех показателей, которые были получены во время сбора данных.
Так, я решила посмотреть ТОП-5 статей по следующим данным:
- Просмотры (по первому и второму показателю просмотров Хабра);
- Число голосов;
- Число добавлений статей в Избранные;
- Число комментариев.
most_viewed1 <- articles_ctsg_full |>
arrange(desc(unique_readers)) |>
head(5)
most_viewed2 <- articles_ctsg_full |>
arrange(desc(reach_value)) |>
head(5)
most_votes <- articles_ctsg_full |>
arrange(desc(votes)) |>
head(5)
most_marked <- articles_ctsg_full |>
arrange(desc(mark))|>
head(5)
most_commented <- articles_ctsg_full |>
arrange(desc(num_of_comments))|>
head(5)Сохраняю ID тех статей, которые являются наиболее значимыми с разных точек зрения (читатели, охват, оценки, комментарии), и формирует итоговый список “ключевых” публикаций компании. После формирую общую таблицу.
all_ids <- unique(c(
most_viewed1$id,
most_viewed2$id,
most_votes$id,
most_marked$id,
most_commented$id
))
result <- tibble(
id = all_ids,
in_most_viewed1 = id %in% most_viewed1$id,
in_most_viewed2 = id %in% most_viewed2$id,
in_most_votes = id %in% most_votes$id,
in_most_marked = id %in% most_marked$id,
in_most_commented = id %in% most_commented$id
)Здесь мы видим, какие статьи входят во все топы, но еще не хватает некоторой структуры.
library(knitr)
library(kableExtra)
library(gt)
result |>
gt() |>
opt_table_outline() |>
tab_options(
table.width = px(500)
) |>
# Цвет текста (чуть темнее базового spacelab)
tab_style(
style = cell_text(color = "#2f3a45"),
locations = cells_body()
) |>
# Шапка таблицы
tab_style(
style = list(
cell_fill(color = "#eef5ff"),
cell_text(weight = "bold", color = "#2f3a45")
),
locations = cells_column_labels()
) |>
# Hover-эффект для строк
opt_css(
css = "
tbody tr:hover {
background-color: #f7fbff;
}
"
)| id | in_most_viewed1 | in_most_viewed2 | in_most_votes | in_most_marked | in_most_commented |
|---|---|---|---|---|---|
| 13 | TRUE | TRUE | FALSE | TRUE | TRUE |
| 11 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 10 | TRUE | TRUE | FALSE | FALSE | TRUE |
| 9 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 12 | TRUE | FALSE | FALSE | FALSE | FALSE |
| 1 | FALSE | TRUE | TRUE | FALSE | FALSE |
| 3 | FALSE | FALSE | TRUE | FALSE | FALSE |
| 4 | FALSE | FALSE | TRUE | FALSE | FALSE |
| 8 | FALSE | FALSE | FALSE | TRUE | FALSE |
| 6 | FALSE | FALSE | FALSE | TRUE | FALSE |
| 2 | FALSE | FALSE | FALSE | FALSE | TRUE |
Поэтому надо просуммировать все строчки и отсортировать по общей сумме. И соединить эту таблицу с последней, чтобы она содержала все данные по каждой “топовой” статье.
result <- result |>
mutate(total_hits = rowSums(across(starts_with("in_")))) |>
arrange(desc(total_hits)) |>
inner_join(articles_ctsg_full, by = "id") |>
select(total_hits, id, title, text, length, link, complexity_label, date, year, month, wday, hour, time, read_time_min, reach_value, unique_readers, votes, mark, num_of_comments, tags, hubs) |>
head(5) |>
mutate(hit_id = row_number()) |>
mutate(hit_id = as.integer(hit_id))
result <- result |>
select(hit_id, total_hits, id, title, text, length, link, complexity_label, date, year, month, wday, hour, time, read_time_min, reach_value, unique_readers, votes, mark, num_of_comments, tags, hubs)Получилась такая таблица. Забрать целую таблицу можно по ссылке. Также дисклеймер, что данные при воспроизведении могут отличаться от представленных.
| hit_id | total_hits | id | title | text | length | link | complexity_label | date | year | month | wday | hour | time | read_time_min | reach_value | unique_readers | votes | mark | num_of_comments | tags | hubs |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 5 | 11 | Распознавание лиц на … | В последние годы появляется … | 2022 | https://habr.com/ru/... | NA | 2024-04-11 | 2024 | Apr | Thu | 17 | 17H 8M 0S | 9 | 5400 | 5400 | 9 | 52 | 7 | информационная безопасность, … | Блог компании … |
| 2 | 5 | 9 | Секреты успешного собеседования: … | Привет! Меня зовут Артём … | 2766 | https://habr.com/ru/... | NA | 2024-05-30 | 2024 | May | Thu | 11 | 11H 37M 0S | 11 | 3800 | 3800 | 7 | 44 | 8 | собеседование в … | Блог компании … |
| 3 | 4 | 13 | Как выжить на … | Всем привет! Меня зовут … | 3348 | https://habr.com/ru/... | NA | 2024-03-29 | 2024 | Mar | Fri | 17 | 17H 36M 0S | 13 | 23000 | 23000 | 3 | 105 | 16 | испытательный срок, … | Блог компании … |
| 4 | 3 | 10 | Как тимлиду проводить … | Всем привет! С вами … | 3032 | https://habr.com/ru/... | NA | 2024-04-17 | 2024 | Apr | Wed | 10 | 10H 48M 0S | 12 | 4000 | 4000 | 3 | 20 | 9 | карьера в … | Блог компании … |
| 5 | 2 | 1 | Как создать решение … | Всем привет! На связи … | 1851 | https://habr.com/ru/... | Средний | 2025-12-02 | 2025 | Dec | Tue | 16 | 16H 44M 0S | 9 | 7300 | 677 | 8 | 9 | 0 | контейнеризация, контейнерная … | Блог компании … |
Итого, в общий ТОП-5 вошли следующие статьи:
- Распознавание лиц на микрокомпьютерах1;
- Секреты успешного собеседования: как получить оффер технарю2;
- Как выжить на первом испытательном сроке в IT и не только3;
- Как тимлиду проводить собеседование так, чтобы кандидат и компания получили от него максимум4;
- Как создать решение в области контейнерной безопасности: подводные камни, проблемы и их решение5.
Чуть позже я попытаюсь сделать выводы о том, почему именно такие статьи оказались наиболоее востребованными у аудитории, предварительно рассмотрев некоторые лексические особенности этих текстов. А пока предлагаю ознакомиться с незамысловатой визуаилизацией, которую я подготовила по обработанным данным :)
Визуализация
Таблицы нам уже могут рассказать некоторую особенность, но визуализация не только дает красивую картинку, но и помогает выявить интересные закономерности в данных. Предлагаю ознакомиться с тем, что у меня получилось!
Сделаю замечание, что здесь представлена статистика по ВСЕМ статьям компании на Хабре, а не только по самым популярным.
Подгружаю библиотеки.
library(gridExtra)
library(grid)
library(paletteer)
library(extrafont)
library(plotly)
library(dplyr)Здесь я хочу посмотреть число статей по месяцам. Для этого сначала посчитаю, сколько статей было написано за определенный год и месяц.
month_order <- c(
"Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"
)
years_order <- c(2024, 2025)
articles_ctsg_full_p1 <- articles_ctsg_full |>
count(year, month, sort = FALSE) |>
tidyr::complete(
year = years_order,
month = month_order,
fill = list(n = 0)
) |>
mutate(
month = factor(month, levels = month_order),
year = factor(year)
) |>
rename(count = n)В целом, это уже готово к визуализации. Только делаю некоторые махинации с палитрой и шрифтом. Предварительно скачиваю фон Montserrat, его можно забрать здесь.
font_import()
loadfonts(device = "win")И готовю график. Сначала через ggplot, потом “оживляю” график через plotly.
p1 <- articles_ctsg_full_p1 |>
mutate(month = factor(month, levels = month_order),
year = factor(year)) |>
ggplot(aes(x = month,
y = count,
fill = year)) +
geom_col(alpha = 0.8, na.rm = TRUE) +
scale_fill_manual(name = "Год",
values = c("2024" = "#3A77B9", "2025" = "#385E8C")) +
scale_x_discrete(labels = month_order) +
labs(title = "Число статей по месяцам",
x = NULL, y = NULL) +
theme(
panel.background = element_rect(fill = "white", color = NA),
panel.grid.major = element_line(color = "grey85", linewidth = 0.4),
panel.grid.minor = element_line(color = "grey92", linewidth = 0.3),
text = element_text(size = 14, family = "Montserrat Medium"))
ggplotly(p1) |>
layout(
legend = list(
x = 1.05,
y = 0.5,
xanchor = "left",
yanchor = "middle",
title = list(
text = " Год"
)
)
)Апрель 2024 года был самым продуктивным годом. 2 из 5 самых популярных статей были опубликованы в этот период.
Теперь рассмотрим статьи по дням недели. Для этого в первую очередь необходимо посчитать число статей по годам и дням недели.
week_order <- c(
"Fri", "Thu", "Wed", "Tue", "Mon"
)
years_order <- c(2024, 2025)
articles_ctsg_full_p2 <- articles_ctsg_full |>
count(year, wday, sort = FALSE) |>
tidyr::complete(
year = years_order,
wday = week_order,
fill = list(n = 0)
) |>
mutate(
year = factor(year)
) |>
rename(count = n)Данные для визуализации готовы! Логика остается та же.
p2 <- articles_ctsg_full_p2 |>
mutate(
wday = factor(wday, levels = week_order),
year = factor(year)
) |>
ggplot(aes(x = wday,
y = count,
fill = year)) +
geom_col(alpha = 0.8, na.rm = TRUE) +
coord_flip() +
scale_fill_manual(
name = "Год",
values = c("2024" = "#3A77B9", "2025" = "#385E8C")
) +
scale_x_discrete(labels = week_order) +
labs(
title = "Статьи по дням недели",
x = NULL, y = NULL
) +
theme(
panel.background = element_rect(fill = "white", color = NA),
panel.grid.major = element_line(color = "grey85", linewidth = 0.4),
panel.grid.minor = element_line(color = "grey92", linewidth = 0.3),
text = element_text(size = 14, family = "Montserrat Medium")
)
ggplotly(p2) |>
layout(
legend = list(
x = 1.05,
y = 0.5,
xanchor = "left",
yanchor = "middle",
title = list(
text = " Год"
)
)
)Самыми “активными” для публикаций днями являются вторник и четверг. Эти дни выбраны не просто так, об этом пишут специалисты6.
Ну и посмотрим последний (в этом разделе!) график, в котором представлены длины статей по годам.
p3 <- articles_ctsg_full |>
ggplot(aes(as.factor(year), length, fill = as.factor(year))) +
geom_boxplot(show.legend = FALSE) +
scale_fill_manual(
name = "Год",
values = c("2024" = "#3A77B9", "2025" = "#385E8C")
) +
labs(title = "Длина статьи по годам") +
labs(x = NULL, y = NULL) +
theme(
panel.background = element_rect(fill = "white", color = NA),
panel.grid.major = element_line(color = "grey85", linewidth = 0.4),
panel.grid.minor = element_line(color = "grey92", linewidth = 0.3),
text = element_text(size = 14, family = "Montserrat Medium")
)
ggplotly(p3) |>
layout(
legend = list(
x = 1.05,
y = 0.5,
xanchor = "left",
yanchor = "middle",
title = list(
text = " Год"
)
)
)Здесь мы видим, что статистически длиннее статьи были в 2024 году. Выбросы в обе стороны также ярко выражены именно в этом году. Могу предложить, что в 2025 году были введены требования оформления статей, которые регламентируют даже длину статьи.
Теперь перейдите на вкладу NLP-анализ, чтобы продолжить изучать мое исследование!
Литература
Сноски
Секреты успешного собеседования: как получить оффер технарю↩︎
Как тимлиду проводить собеседование так, чтобы кандидат и компания получили от него максимум↩︎
Как создать решение в области контейнерной безопасности: подводные камни, проблемы и их решение↩︎
Когда лучше всего публиковать статьи в блог (Статистика из США и России)↩︎